/*
Description : Command line interface to control and monitor a relayduino board
			  (like kta223) thru its FTDI usb/serial adapter.
Author      : Samuel Ventura <samuel.ventura@yeico.com>
Copyright   : (c) 2011 Samuel Ventura. All rights reserved.
License 	: http://www.opensource.org/licenses/Apache-2.0
*/

using GLib;

namespace relayduino {

	public class Kta223 {

		private bool is_socket = false;
		private int timeout = 2 * 1000; /* 2 seconds timeout */
		private int fd;
		private bool debug;
		private int baudrate;
		private int address;
		private Posix.termios old;
		public int dinputs = 0;
		public int doutputs = 0;
		public int[] ainputs = new int[] {0, 0, 0, 0};
		
		public Kta223.serial(int fd, int baudrate, int address, bool debug) {
			this.fd = fd;
			this.debug = debug;
			this.baudrate = baudrate;
			this.address = address;
		}
		
		public Kta223.socket(int fd, int address, bool debug) {
			this.fd = fd;
			this.debug = debug;
			this.address = address;
			this.is_socket = true;
		}

		public void restore_serial() {
			if (Posix.tcsetattr(fd, Posix.TCSANOW, old) < 0) {
				stderr.printf("Unable to restore kta termios\n");
			}
			if (Posix.close(fd) < 0) {
				stderr.printf("Unable to close kta file\n");
			}
		}

		public int setup_serial() {
		    if (Posix.tcgetattr(fd, out old) < 0) {
				stderr.printf("Unable to get kta termios\n");
				return -1;
			}
			Posix.termios tio = Posix.termios();
			switch(baudrate) {
				case 1200 : tio.c_cflag = Posix.B1200; break;
				case 2400 : tio.c_cflag = Posix.B2400; break;
				case 4800 : tio.c_cflag = Posix.B4800; break;
				case 9600 : tio.c_cflag = Posix.B9600; break;
				//case 14400 : tio.c_cflag = Posix.B14400; break; /* undefined */
				case 19200 : tio.c_cflag = Posix.B19200; break;
				//case 28800 : tio.c_cflag = Posix.B28800; break; /* undefined */
				case 38400 : tio.c_cflag = Posix.B38400; break;
				case 57600 : tio.c_cflag = Posix.B57600; break;
				case 115200 : tio.c_cflag = Posix.B115200; break;
			}
			//8N1
			tio.c_cflag |= Posix.CS8 | Posix.CLOCAL | Posix.CREAD;
			//raw output
			tio.c_oflag = 0;
			//raw input for read timeout
			tio.c_lflag = ~(Posix.ICANON | Posix.ECHO);
			tio.c_cc[Posix.VMIN] = 0; /* immediate return if no data */
			tio.c_cc[Posix.VTIME] = 0;
            if (Posix.tcsetattr(fd, Posix.TCSANOW, tio) < 0) {
				stderr.printf("Unable to set kta termios\n");
				return -1;
			}
			return 0;
		}

		public void restore_socket() {
			if (Posix.close(fd) < 0) {
				stderr.printf("Unable to close kta file\n");
			}
		}

		public int setup_socket() {
			return 0;
		}

		public int read_all() {
			if (read_doutputs() < 0) {
				stderr.printf("Failed to read_doutputs\n");				
				return -1;
			}
			if (read_dinputs() < 0) {
				stderr.printf("Failed to read_dinputs\n");				
				return -2;
			}
			if (read_ainputs() < 0) {
				stderr.printf("Failed to read_ainputs\n");				
				return -3;
			}
			return 0;
		}

		public int read_doutputs() {
			var cmd = "@%02d RS 0\r".printf(address);
			if (send_command(cmd) < 0) {
				stderr.printf("Failed to send_command\n");				
				return -1;
			}
			var res = read_line();
			if (res == null) {
				stderr.printf("Null response\n");				
				return -2;
			}
			var r = new GLib.Regex("^#%02d \\d+\r\n$".printf(address));
			if (!r.match(res)) {
				stderr.printf("Unexpected response '%s'/(%s) for '%s'\n".printf(fix(res), hex(res), cmd.strip()));
				return -3;
			}
			var parts = res.split(" ");
			doutputs = int.parse(parts[1].strip());
			return 0;
		}

		public int read_dinputs() {
			var cmd = "@%02d IS 0\r".printf(address);
			if (send_command(cmd) < 0) {
				stderr.printf("Failed to send_command\n");				
				return -1;
			}
			var res = read_line();
			if (res == null) {
				stderr.printf("Null response\n");				
				return -2;
			}
			var r = new GLib.Regex("^#%02d \\d+\r\n$".printf(address));
			if (!r.match(res)) {
				stderr.printf("Unexpected response '%s'/(%s) for '%s'\n".printf(fix(res), hex(res), cmd.strip()));
				return -3;
			}
			var parts = res.split(" ");
			dinputs = int.parse(parts[1].strip());
			return 0;
		}

		public int read_ainputs() {
			var cmd = "@%02d AI 0\r".printf(address);
			if (send_command(cmd) < 0) {
				stderr.printf("Failed to send_command\n");				
				return -1;
			}
			var res = read_line();
			if (res == null) {
				stderr.printf("Null response\n");				
				return -2;
			}
			var r = new GLib.Regex("^#%02d \\d+ \\d+ \\d+\r\n$".printf(address));
			if (!r.match(res)) {
				stderr.printf("Unexpected response '%s'/(%s) for '%s'\n".printf(fix(res), hex(res), cmd.strip()));
				return -3;
			}
			var parts = res.split(" ");
			ainputs = new int[] {int.parse(parts[1].strip()), 
				int.parse(parts[2].strip()),
				int.parse(parts[3].strip())};
			return 0;
		}

		public int write_doutputs(int outs) {
			var cmd = "@%02d WR %d\r".printf(address, outs);
			if (send_command(cmd) < 0) {
				stderr.printf("Failed to send_command\n");				
				return -1;
			}
			var res = read_line();
			if (res == null) {
				stderr.printf("Null response\n");				
				return -2;
			}
			if (res != "#00\r\n") {
				stderr.printf("Unexpected response '%s'/(%s) for '%s'\n".printf(fix(res), hex(res), cmd.strip()));
				return -3;
			}
			return 0;
		}

		public int change_address(int addrs) {
			var cmd = "@%02d SA %02d\r".printf(address, addrs);
			if (send_command(cmd) < 0) {
				stderr.printf("Failed to send_command\n");				
				return -1;
			}
			var res = read_line();
			if (res == null) {
				stderr.printf("Null response\n");				
				return -2;
			}
			if (res != "#00\r\n") {
				stderr.printf("Unexpected response '%s'/(%s) for '%s'\n".printf(fix(res), hex(res), cmd.strip()));
				return -3;
			}
			return 0;
		}

		private int send_command(string cmd) {
			log("out", cmd);
			if (is_socket) {
				return send_command_socket(cmd);
			} else {
				return send_command_serial(cmd);
			}
		}

		private string? read_line() {
			string str;
			if (is_socket) {
				str = read_line_socket();
			} else {
				str = read_line_serial();
			}
			log("in", str);
			return str;
		}

		private int send_command_serial(string cmd) {
			/* discard input/output buffers */
			if (Posix.tcflush(fd, Posix.TCIOFLUSH) < 0) return -1;
			/* send command */
			var c = Posix.write(fd, cmd.data, cmd.length);
			if (c != cmd.length) return -2;
			/* wait output buffer is empty */
			if (Posix.tcdrain(fd) < 0) return -3;
			return 0;
		}

		private int send_command_socket(string cmd) {
			/* send command */
			var c = Posix.send(fd, cmd.data, cmd.length, 0);
			if (c != cmd.length) {
				stderr.printf("Send returned %d\n", (int)c);			
				return -2;
			}
			return 0;
		}

		private string? read_line_serial() {
			var data = new uchar[32];
			var pos = 0;
			var ts = Posix.timespec();
			Posix.clock_gettime(Posix.CLOCK_REALTIME, out ts);
			long cur = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
			long dl = cur + timeout;
			while(cur < dl && pos < 31) { /* leave space for zero string end */
				var c = Posix.read(fd, &data[pos], 1);
				if (c < 0 || c > 1) return null; /* 0 means EOF -1 means error*/
				if (c == 1) {
					pos++;
					if (data[pos-1] == '\n') break;
				} else {
					Posix.usleep(10 * 1000); /* 10 millis */
					Posix.clock_gettime(Posix.CLOCK_REALTIME, out ts);
					cur = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
				}
			}
			data[pos] = 0;
			return (string)data;
		}

		private string? read_line_socket() {
			var data = new uchar[32];
			var pos = 0;
			var ts = Posix.timespec();
			Posix.clock_gettime(Posix.CLOCK_REALTIME, out ts);
			long cur = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
			long dl = cur + timeout;
			while(cur < dl && pos < 31) { /* leave space for zero string end */
				var c = Posix.recv(fd, &data[pos], 1, 0);
				if (c < 0 || c > 1) return null; /* 0 means EOF -1 means error*/
				if (c == 1) {
					pos++;
					if (data[pos-1] == '\n') break;
				} else {
					Posix.usleep(10 * 1000); /* 10 millis */
					Posix.clock_gettime(Posix.CLOCK_REALTIME, out ts);
					cur = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
				}
			}
			data[pos] = 0;
			return (string)data;
		}

		private void log(string dir, string line) {
			if (!debug) return;
			switch(dir) {
				case "in" : stdout.printf("<%s (%s)\n", line.strip(), hex(line)); break;
				case "out" : stdout.printf(">%s (%s)\n", line.strip(), hex(line)); break;
			}
		}

		private string hex(string str) {
			StringBuilder buffer = new StringBuilder();
			for(int i=0;i<str.length;i++) {
				if (i > 0) buffer.append_c(' ');
				var ch = str.data[i];
				if (ch < 16) buffer.append_c('0');
				buffer.append_printf("%X", ch);
			}
			return buffer.str;
		}

		private string fix(string str) {
			StringBuilder buffer = new StringBuilder();
			for(int i=0;i<str.length;i++) {
				var ch = str.data[i];
				if (!Posix.isgraph(ch)) ch = ' ';
				buffer.append_c((char)ch);
			}
			return buffer.str;
		}
	}
}
